// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.unit;

import bats.SpatialProperties;
import bats.control.physics.GrowControl;
import bats.missile.Shafts;
import bats.scene.SceneNode;
import bats.scene.WorldNode;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.Misc;
import jme3utilities.SimpleControl;
import jme3utilities.math.MyMath;

/**
 * A simple control (applied to units) to load and launch missiles. Missiles
 * must be loaded before they can be launched.
 * <p>
 * The control invokes the unit's OtoAnimationControl to play the "pull"
 * animation as appropriate.
 * <p>
 * Each instance is disabled at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class LaunchControl
        extends SimpleControl {
    // *************************************************************************
    // constants

    // TODO some of these should be fields with accessors
    /**
     * elevation angle tolerance (in radians)
     */
    final private static float elevationTolerance = 0.01f;
    /**
     * phase at which the missile is ready to be launched (in seconds) NOTE this
     * should be greater than loadDuration.
     */
    final static float launchPhase = 1f;
    /**
     * minimum time interval to fully load a missile (in seconds)
     */
    final static float loadDuration = 0.4f;
    /**
     * maximum elevation angle magnitude (radians above or below the horizon)
     */
    final private static float maxElevation = FastMath.PI / 3f; // 60 degrees
    /**
     * minimum time interval to recover from launching a missile (in seconds)
     */
    final private static float recoveryInterval = 0.5f + Misc.blendTime;
    /**
     * phase at which the launcher is fully recovered (in seconds)
     */
    final private static float recoveredPhase = launchPhase + recoveryInterval;
    /**
     * length of missile launched by a standard-size unit (in meters)
     */
    final private static float standardLength = 1.2f;
    /**
     * radius of missile launched by a standard-size unit (in meters)
     */
    final private static float standardRadius = 0.02f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(LaunchControl.class.getName());
    /**
     * model coordinates of the launch point
     */
    final private static Vector3f launchOffset =
            new Vector3f(-2.3f, 5.2f, 0.7f);
    // *************************************************************************
    // fields
    /**
     * true if a load is requested; false if a load is not requested
     */
    private boolean isLoadRequested = false;
    /**
     * missile angle above the horizontal (in radians), or null if no load or
     * launch has been requested
     */
    private Float elevationAngle = null;
    /**
     * requested launch speed (in meters/second) or zero if no launch requested
     */
    private float launchSpeed = 0f;
    /**
     * phase of the load/launch cycle (in seconds)
     */
    private float phase = 0f;
    /**
     * the current missile, or null if there's no missile loaded
     */
    private Spatial missile = null;
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled control.
     */
    public LaunchControl() {
        clearRequests();
        super.setEnabled(false);
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Cancel the launch.
     */
    public void abort() {
        missile = null;
        elevationAngle = null;
        if (phase == launchPhase) {
            phase += 0.01f;
        }
    }

    /**
     * Get the maximum launch speed for this launcher.
     *
     * @return speed (in meters/second)
     */
    float getMaxLaunchSpeed() {
        Otos otos = WorldNode.find(spatial).getOtos();
        return otos.getMaxLaunchSpeed(spatial);
    }

    /**
     * Get the spatial of the current missile.
     *
     * @return the spatial, or null if no missile
     */
    public Spatial getMissile() {
        return missile;
    }

    /**
     * Test whether the unit has a missile fully loaded and ready to launch.
     *
     * @return true if ready to launch, false if not ready
     */
    public boolean isReadyToLaunch() {
        if (missile == null) {
            return false;
        }
        if (!isEnabled()) {
            return false;
        }
        if (phase != launchPhase) {
            return false;
        }
        return true;
    }

    /**
     * Request that the current missile be launched.
     *
     * @param speed (in meters/second)
     * @param requestedElevation (in radians)
     */
    void requestLaunch(float speed, float requestedElevation) {
        assert isEnabled();
        assert phase == launchPhase : phase;
        assert missile != null;
        assert !SpatialProperties.isHidden(missile);

        setElevation(requestedElevation);
        /*
         * Ignore request if requested speed is invalid.
         */
        float maxLaunchSpeed = getMaxLaunchSpeed();
        if (speed <= 0f || speed > maxLaunchSpeed) {
            logger.log(Level.INFO,
                    "invalid launch request: speed={0} maxSpeed={1}",
                    new Object[]{
                speed, maxLaunchSpeed
            });
            launchSpeed = 0f;
            return;
        }
        /*
         * Set launchSpeed to a positive value launch the missile during
         * the next update.
         */
        logger.log(Level.INFO, "ready {0}", spatial.getName());
        launchSpeed = speed;
    }

    /**
     * Request that the unit load a missile.
     *
     * @param requestedElevation missile's angle above the horizontal (in
     * radians)
     */
    void requestLoad(float requestedElevation) {
        assert missile == null;
        /*
         * Limit the elevation angle.
         */
        float newAngle = MyMath.clamp(requestedElevation, maxElevation);
        elevationAngle = newAngle;
        /*
         * Set isLoadRequested to load a missile during
         * the next update.
         */
        isLoadRequested = true;
    }
    // *************************************************************************
    // SimpleControl methods

    /**
     * Callback invoked when the spatial's geometric state is about to be
     * updated, once per frame while attached and enabled.
     *
     * @param updateInterval time interval between updates (in seconds, &ge;0)
     */
    @Override
    protected void controlUpdate(float updateInterval) {
        super.controlUpdate(updateInterval);
        if (spatial == null || !SceneNode.find(spatial).isEnabled()) {
            return;
        }
        logger.log(Level.FINE,
                "{0} updateInterval = {1}, phase = {2}, launchSpeed = {3}",
                new Object[]{
            spatial.getName(), updateInterval, phase, launchSpeed
        });
        /*
         * Limit the time interval to avoid skipping steps.
         */
        float simInterval =
                FastMath.clamp(updateInterval, 0f, SceneNode.maxSimInterval);
        /*
         * Load and launch missiles based on requests.
         */
        if (phase == 0f) {
            /*
             * The launcher is ready to begin loading a missile.
             */
            if (isLoadRequested && SpatialProperties.hasMoreAmmo(spatial)) {
                SpatialProperties.consumeAmmo(spatial);
                /*
                 * Start loading a missile.
                 */
                phase += simInterval;
            }

        } else if (phase < loadDuration) {
            /*
             * still loading the missile ...
             */
            phase += simInterval;

        } else if (phase < launchPhase) {
            if (missile == null) {
                /*
                 * Create the missile.
                 */
                loadMissile();

            } else {
                SpatialProperties.reveal(missile);
                phase = launchPhase;
            }

        } else if (phase == launchPhase) {
            assert isReadyToLaunch();
            if (launchSpeed > 0f) {
                launchMissile();
                /*
                 * Start recovering from the launch.
                 */
                phase += simInterval;
            }

        } else if (phase < recoveredPhase) {
            /*
             * still recovering from the launch ...
             */
            phase += simInterval;

        } else {
            /*
             * The launcher has recovered from the launch and
             * is ready to reload.
             */
            phase = 0f;
        }
        OtoAnimationControl otoAnimationControl =
                spatial.getControl(OtoAnimationControl.class);
        if (otoAnimationControl != null) {
            otoAnimationControl.setLaunchPhase(phase);
        }

        clearRequests();
    }
    // *************************************************************************
    // private methods

    /**
     * Clear all pending requests.
     */
    private void clearRequests() {
        isLoadRequested = false;
        launchSpeed = 0f;
    }

    /**
     * Launch the loaded missile. Invoked during update.
     */
    private void launchMissile() {
        assert isEnabled();
        assert phase == launchPhase : phase;
        assert missile != null;
        assert !SpatialProperties.isHidden(missile);
        assert launchSpeed > 0f : launchSpeed;

        WorldNode world = WorldNode.find(spatial);
        world.getShafts().launch(spatial, missile, launchSpeed);

        missile = null;
        UnitClass unitClass = world.getUnitClass(spatial);
        unitClass.updateCollisionShape(spatial);
    }

    /**
     * Create a loaded missile. Invoked during update.
     */
    private void loadMissile() {
        assert missile == null : missile;

        WorldNode world = WorldNode.find(spatial);
        Otos otos = world.getOtos();
        float launcherRelativeSize = otos.getRelativeSize(spatial);
        float length = launcherRelativeSize * standardLength; // meters
        float radius = launcherRelativeSize * standardRadius; // meters

        Shafts shafts = world.getShafts();
        Node launcher = (Node) spatial;
        missile = shafts.load(launcher, length, radius, elevationAngle,
                launchOffset);
        assert missile != null;

        updateCollisionShape();
    }

    /**
     * Alter the elevation angle of an existing missile.
     *
     * @param requestedElevation angle above the horizontal (in radians)
     */
    private void setElevation(float requestedElevation) {
        assert isEnabled();
        assert missile != null;
        assert elevationAngle != null;

        float newElevation = MyMath.clamp(requestedElevation, maxElevation);
        float elevationError = FastMath.abs(elevationAngle - newElevation);
        if (elevationError < elevationTolerance) {
            /*
             * The missile's current elevation is close enough.
             */
            return;
        }
        elevationAngle = newElevation;
        Shafts shafts = WorldNode.find(spatial).getShafts();
        shafts.setElevation(missile, elevationAngle);

        updateCollisionShape();
    }

    /**
     * Update the launcher's collision shape.
     */
    private void updateCollisionShape() {
        GrowControl growControl = spatial.getControl(GrowControl.class);
        OtoControl otoControl = spatial.getControl(OtoControl.class);
        if (!growControl.isEnabled()) {
            /*
             * Enable the launcher's grow control.
             */
            growControl.addSuccessor(otoControl);
            growControl.addSuccessor(this);
            growControl.setEnabled(true);
        }
        /*
         * Specify a new collision shape for GrowControl.
         */
        CollisionShape shape = otoControl.createShape();
        growControl.setCollisionShape(shape);

        setEnabled(false);
    }
}